﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Xrm.Sdk.Query;
using System.ServiceModel;
using System.Runtime.Caching;

namespace NNAssociationTool.Plugin
{
    /// <summary>
    /// Associate (Disassociate) Entity Records Plugin
    /// 
    /// if plugin fail operation, replace entity1 and entity2
    /// value each other.
    /// </summary>
    public class ManyToManyRelationshipPlugin : IPlugin
    {
        #region ManyToManyRelationship Fieled Name Constant Field
        private static readonly string Np_manytomanyrelationship_schema_name = "np_manytomanyrelationship_schema_name";
        private static readonly string Np_entity1 = "np_entity1";
        private static readonly string Np_entity2 = "np_entity2";
        private static readonly string Np_operationtype = "np_operationtype";
        private static readonly string Np_remark = "np_remark";
        private static readonly string Np_name = "np_name";
        #endregion

        #region Plugin Context Parameter
        private static readonly string PrimaryEntityName = "np_manytomanyrelationship";
        private static readonly string CreateMessage = "Create";
        private static readonly string Target = "Target";
        #endregion

        #region OptionSetValue
        private static readonly int OperationType_Associate = 800000000;
        private static readonly int OperationType_Disassociate = 800000001;
        #endregion

        #region Cache
        /// <summary>
        /// Object Cache for ManyToManyRelationshipMetadata
        /// </summary>
        private static MemoryCache MetadataCache = new MemoryCache("NNAssociationToolPluginCache");
        #endregion

        /// <summary>
        /// Plugin Entry Point.
        /// </summary>
        /// <param name="serviceProvider"></param>
        public void Execute(IServiceProvider serviceProvider)
        {
            try
            {
                ValidateContext(serviceProvider);
                ProcessOperation(serviceProvider);
            }
            catch (Exception e)
            {
                if (e is InvalidPluginExecutionException) throw;
                else throw new InvalidPluginExecutionException(e.Message, e);
            }
        }

        /// <summary>
        /// Execute Associate or Disassociate Operation
        /// </summary>
        /// <param name="serviceProvider"></param>
        private void ProcessOperation(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = serviceProvider.GetPluginExecutionContext();
            Entity entity = context.InputParameters[ManyToManyRelationshipPlugin.Target] as Entity;
            string relationshipSchemaName = entity.GetAttributeValue<string>(ManyToManyRelationshipPlugin.Np_manytomanyrelationship_schema_name);
            string entity1 = entity.GetAttributeValue<string>(ManyToManyRelationshipPlugin.Np_entity1);
            string entity2 = entity.GetAttributeValue<string>(ManyToManyRelationshipPlugin.Np_entity2);
            OptionSetValue operationType = entity.GetAttributeValue<OptionSetValue>(ManyToManyRelationshipPlugin.Np_operationtype);

            IOrganizationServiceFactory factory = serviceProvider.GetOrganizatinoServiceFactory();
            IOrganizationService service = factory.CreateOrganizationService(null); // call as SYSTEM             

            try
            {
                ManyToManyRelationshipMetadata metadata = GetManyToManyMetadata(service, relationshipSchemaName);
                Guid entity1id = GetEntityId(service, metadata.Entity1LogicalName, entity1);
                Guid entity2id = GetEntityId(service, metadata.Entity2LogicalName, entity2);

                if (operationType.Value == ManyToManyRelationshipPlugin.OperationType_Associate)
                {
                    Associate(service, relationshipSchemaName, metadata.Entity1LogicalName, entity1id,
                        metadata.Entity2LogicalName, entity2id);
                }
                else if (operationType.Value == ManyToManyRelationshipPlugin.OperationType_Disassociate)
                {
                    Disassociate(service, relationshipSchemaName, metadata.Entity1LogicalName, entity1id,
                        metadata.Entity2LogicalName, entity2id);
                }
                SetPrimaryFieldValue(entity, operationType.Value, relationshipSchemaName, entity1, entity2);
            }
            catch (FaultException<OrganizationServiceFault> fe)
            {
                if (fe.Detail.ErrorCode == -2147220937)
                {
                    AppendRemark(entity, "relationship is alread exists.");
                }
                else
                {
                    throw fe;
                }

            }
        }

        /// <summary>
        /// Add Remark to Remark field.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="appendMessage"></param>
        private void AppendRemark(Entity entity, string appendMessage)
        {
            StringBuilder builder = new StringBuilder();
            if (entity.Attributes.ContainsKey(ManyToManyRelationshipPlugin.Np_remark))
            {
                string s = entity.GetAttributeValue<string>(ManyToManyRelationshipPlugin.Np_remark);
                if (s != null)
                {
                    builder.AppendLine(s);
                }
            }

            builder.AppendLine(appendMessage);

            entity.Attributes[ManyToManyRelationshipPlugin.Np_remark] = builder.ToString();
        }
        /// <summary>
        /// Set many to many relationship primary field value
        /// </summary>
        /// <param name="entity">entity</param>
        /// <param name="operationType">OperationType</param>
        /// <param name="relationshipSchemaName">Relationship Schema name</param>
        /// <param name="entity1">entity1 value</param>
        /// <param name="entity2">entity2 value</param>
        private void SetPrimaryFieldValue(Entity entity, int operationType, string relationshipSchemaName, string entity1, string entity2)
        {
            string format = "{0}_by_{1}_{2}_{3}";
            string operationStr = null;
            if (operationType == ManyToManyRelationshipPlugin.OperationType_Associate)
            {
                operationStr = "Associate";
            }
            else
            {
                operationStr = "Disassociate";
            }
            entity[ManyToManyRelationshipPlugin.Np_name] = string.Format(format, operationStr, relationshipSchemaName, entity1, entity2);
        }
        /// <summary>
        /// Associate Entitities
        /// </summary>
        /// <param name="service">IOrganizatinoService</param>
        /// <param name="relationshipSchemaName">Relationship Schema Name</param>
        /// <param name="entity1LogicalName">entity logical name</param>
        /// <param name="entity1id">entity record id</param>
        /// <param name="entity2LogicalName">another entity logical name</param>
        /// <param name="entity2id">another entity record id</param>
        private void Associate(IOrganizationService service, string relationshipSchemaName
            , string entity1LogicalName, Guid entity1id, string entity2LogicalName, Guid entity2id)
        {
            EntityReference entity1 = new EntityReference(entity1LogicalName, entity1id);
            EntityReferenceCollection entity2 = new EntityReferenceCollection();
            entity2.Add(new EntityReference(entity2LogicalName, entity2id));
            Relationship relationship = new Relationship(relationshipSchemaName);

            AssociateRequest request = new AssociateRequest
            {
                Relationship = relationship,
                Target = entity1,
                RelatedEntities = entity2
            };
            service.Execute(request);
        }

        /// <summary>
        /// Disassociate Entities
        /// </summary>
        /// <param name="service">IOrganizatinoService</param>
        /// <param name="relationshipSchemaName">Relationship Schema Name</param>
        /// <param name="entity1LogicalName">entity logical name</param>
        /// <param name="entity1id">entity record id</param>
        /// <param name="entity2LogicalName">another entity logical name</param>
        /// <param name="entity2id">another entity record id</param>
        private void Disassociate(IOrganizationService service, string relationshipSchemaName
            , string entity1LogicalName, Guid entity1id, string entity2LogicalName, Guid entity2id)
        {
            EntityReference entity1 = new EntityReference(entity1LogicalName, entity1id);
            EntityReferenceCollection entity2 = new EntityReferenceCollection();
            entity2.Add(new EntityReference(entity2LogicalName, entity2id));
            Relationship relationship = new Relationship(relationshipSchemaName);

            DisassociateRequest request = new DisassociateRequest
            {
                Relationship = relationship,
                Target = entity1,
                RelatedEntities = entity2
            };
            service.Execute(request);
        }

        /// <summary>
        /// Get Entity Record Id
        /// </summary>
        /// <param name="service">IOrganizationService</param>
        /// <param name="entityLogicalName">entity logical name</param>
        /// <param name="value">record guid or primary field value</param>
        /// <returns>record id</returns>
        private Guid GetEntityId(IOrganizationService service, string entityLogicalName, string value)
        {
            Guid id;
            // if value is guid then return as record id
            if (Guid.TryParse(value, out id))
            {
                return id;
            }
            // retrieve by primary field
            EntityMetadata metadata = GetEntityMetadata(service, entityLogicalName);
            string primaryField = metadata.PrimaryNameAttribute;
            string primaryId = metadata.PrimaryIdAttribute;

            // Retrieve Entity Record Id
            QueryExpression query = new QueryExpression(entityLogicalName);
            query.NoLock = true;
            query.ColumnSet = new ColumnSet();
            ConditionExpression condition = new ConditionExpression(primaryField, ConditionOperator.Equal, value);
            query.Criteria.AddCondition(condition);

            RetrieveMultipleRequest request = new RetrieveMultipleRequest
            {
                Query = query,
            };
            RetrieveMultipleResponse response = service.Execute(request) as RetrieveMultipleResponse;
            EntityCollection entities = response.EntityCollection;
            if (entities.Entities.Count != 1)
            {
                ThrowException(string.Format("{0} record(s) was found of entity {1} and value {2}.", entities.Entities.Count, entityLogicalName, value));
            }
            return entities.Entities[0].GetAttributeValue<Guid>(primaryId);
        }
        /// <summary>
        /// Get ManyToManyRelationshipMetadata
        /// </summary>
        /// <param name="service">IOrganizatinoService</param>
        /// <param name="relationshipSchemaName">Many to Many Relation schema name</param>
        /// <returns>Metadata</returns>
        private ManyToManyRelationshipMetadata GetManyToManyMetadata(IOrganizationService service, string relationshipSchemaName)
        {
            ManyToManyRelationshipMetadata metadata = ManyToManyRelationshipPlugin.MetadataCache[relationshipSchemaName] as ManyToManyRelationshipMetadata;
            if (metadata == null)
            {
                RetrieveRelationshipRequest request = new RetrieveRelationshipRequest
                {
                    Name = relationshipSchemaName
                };
                RetrieveRelationshipResponse response = service.Execute(request) as RetrieveRelationshipResponse;
                metadata = response.RelationshipMetadata as ManyToManyRelationshipMetadata;

                if (metadata == null)
                {
                    ThrowException(string.Format("relationship schema name {0} is not found", relationshipSchemaName));
                }

                // Chache Item 
                CacheItemPolicy policy = new CacheItemPolicy();
                policy.SlidingExpiration = TimeSpan.FromMinutes(2); // 2 min sliding policy
                ManyToManyRelationshipPlugin.MetadataCache.Set(relationshipSchemaName, metadata, policy);
            }

            return metadata;
        }

        /// <summary>
        /// Get Entity Metadata
        /// </summary>
        /// <param name="service">IOrganizationService</param>
        /// <param name="entityLogicalName">EntityLogicalName</param>
        /// <returns>Entity Metadata</returns>
        private EntityMetadata GetEntityMetadata(IOrganizationService service, string entityLogicalName)
        {
            RetrieveEntityRequest request = new RetrieveEntityRequest
            {
                EntityFilters = EntityFilters.Entity,
                LogicalName = entityLogicalName
            };

            RetrieveEntityResponse response = service.Execute(request) as RetrieveEntityResponse;
            return response.EntityMetadata;
        }
        /// <summary>
        /// Validate Plugin Execution Context
        /// </summary>
        /// <param name="serviceProvider"></param>
        private void ValidateContext(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = serviceProvider.GetPluginExecutionContext();
            // check trigger message
            if (!context.MessageName.Equals(ManyToManyRelationshipPlugin.CreateMessage, StringComparison.InvariantCultureIgnoreCase))
            {
                ThrowException("unsupported message");
            }
            // check trigger entity
            if (!context.PrimaryEntityName.Equals(ManyToManyRelationshipPlugin.PrimaryEntityName, StringComparison.InvariantCultureIgnoreCase))
            {
                ThrowException("unsupported entity");
            }

            Entity entity = context.InputParameters[ManyToManyRelationshipPlugin.Target] as Entity;
            if (!entity.Attributes.Contains(ManyToManyRelationshipPlugin.Np_manytomanyrelationship_schema_name))
            {
                ThrowException(string.Format("{0} is required.", ManyToManyRelationshipPlugin.Np_manytomanyrelationship_schema_name));
            }
            if (!entity.Attributes.Contains(ManyToManyRelationshipPlugin.Np_entity1))
            {
                ThrowException(string.Format("{0} is required.", ManyToManyRelationshipPlugin.Np_entity1));
            }
            if (!entity.Attributes.Contains(ManyToManyRelationshipPlugin.Np_entity2))
            {
                ThrowException(string.Format("{0} is required.", ManyToManyRelationshipPlugin.Np_entity2));
            }
            if (!entity.Attributes.Contains(ManyToManyRelationshipPlugin.Np_operationtype))
            {
                // if not set, operation type is set to associate.
                entity.Attributes[ManyToManyRelationshipPlugin.Np_operationtype] = new OptionSetValue(ManyToManyRelationshipPlugin.OperationType_Associate);
            }
        }

        /// <summary>
        /// Exception Helper Method
        /// </summary>
        /// <param name="message">error message</param>
        /// <param name="innerException">inner exception</param>
        private void ThrowException(string message, Exception innerException = null)
        {
            throw new InvalidPluginExecutionException(message, innerException);
        }
    }
}
